2D Visualization笔记(X2) OxyPlot(三)

除了前面两篇笔记中提到的使用PlotView和PlotModel, OxyPlot.WPF还支持直接在XAML中绑定数据, 使用的是Plot控件, Plot和PlotView一样, 都继承自PlotBase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:oxy="http://oxyplot.org/wpf"
xmlns:local="clr-namespace:WpfApplication2"
Title="Example 2 (WPF)" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<oxy:Plot Title="{Binding Title}">
<oxy:Plot.Series>
<oxy:LineSeries ItemsSource="{Binding Points}"/>
</oxy:Plot.Series>
</oxy:Plot>
</Grid>
</Window>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace WpfApplication2
{
using System.Collections.Generic;
using OxyPlot;

public class MainViewModel
{
public MainViewModel()
{
this.Title = "Example 2";
this.Points = new List<DataPoint>
{
new DataPoint(0, 4),
new DataPoint(10, 13),
new DataPoint(20, 15),
new DataPoint(30, 16),
new DataPoint(40, 12),
new DataPoint(50, 12)
};
}

public string Title { get; private set; }
public IList<DataPoint> Points { get; private set; }
}
}

oxy:Plot.Series和oxy:LineSeries直接放在XAML中? Series和LineSeries不是在OxyPlot(Portable)中吗?

答案是这里的Series和LineSeries都继承自ItemControl类, 位于OxyPlot.WPF中, 是一个控件

1
2
3
4
5
6
public abstract class Series : ItemsControl

/// <summary>
/// This is a WPF wrapper of OxyPlot.LineSeries
/// </summary>
public class LineSeries : DataPointSeries

从LineSeries的注释可以看出, 它是对OxyPlot.LineSeries的一个封装. 再来看Plot类, 它的实现逻辑和PlotView不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
public partial class Plot : PlotBase
{
/// <summary>
/// The internal model.
/// </summary>
private readonly PlotModel internalModel;

/// <summary>
/// Initializes a new instance of the <see cref="Plot" /> class.
/// </summary>
public Plot()
{
this.series = new ObservableCollection<Series>();
this.axes = new ObservableCollection<Axis>();
this.annotations = new ObservableCollection<Annotation>();

this.series.CollectionChanged += this.OnSeriesChanged;
this.axes.CollectionChanged += this.OnAxesChanged;
this.annotations.CollectionChanged += this.OnAnnotationsChanged;

this.defaultController = new PlotController();
this.internalModel = new PlotModel();
((IPlotModel)this.internalModel).AttachPlotView(this);
}

/// <summary>
/// Called when series is changed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs" /> instance containing the event data.</param>
private void OnSeriesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.SyncLogicalTree(e);
}

/// <summary>
/// Synchronizes the logical tree.
/// </summary>
/// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs" /> instance containing the event data.</param>
private void SyncLogicalTree(NotifyCollectionChangedEventArgs e)
{
// In order to get DataContext and binding to work with the series, axes and annotations
// we add the items to the logical tree
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
this.AddLogicalChild(item);
}
}

if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
this.RemoveLogicalChild(item);
}
}
}

/// <summary>
/// Updates the model. If Model==<c>null</c>, an internal model will be created. The ActualModel.Update will be called (updates all series data).
/// </summary>
/// <param name="updateData">if set to <c>true</c> , all data collections will be updated.</param>
protected override void UpdateModel(bool updateData = true)
{
this.SynchronizeProperties();
this.SynchronizeSeries();
this.SynchronizeAxes();
this.SynchronizeAnnotations();

base.UpdateModel(updateData);
}

/// <summary>
/// Called when the visual appearance is changed.
/// </summary>
protected void OnAppearanceChanged()
{
this.InvalidatePlot(false);
}

/// <summary>
/// Called when the visual appearance is changed.
/// </summary>
/// <param name="d">The d.</param>
/// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
private static void AppearanceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Plot)d).OnAppearanceChanged();
}

/// <summary>
/// Synchronizes the series in the internal model.
/// </summary>
private void SynchronizeSeries()
{
this.internalModel.Series.Clear();
foreach (var s in this.Series)
{
this.internalModel.Series.Add(s.CreateModel());
}
}

/// <summary>
/// Synchronize properties in the internal Plot model
/// </summary>
private void SynchronizeProperties()
{
var m = this.internalModel;

m.PlotType = this.PlotType;

m.PlotMargins = this.PlotMargins.ToOxyThickness();
m.Padding = this.Padding.ToOxyThickness();
m.TitlePadding = this.TitlePadding;

m.Culture = this.Culture;

m.DefaultColors = this.DefaultColors.Select(c => c.ToOxyColor()).ToArray();
m.DefaultFont = this.DefaultFont;
m.DefaultFontSize = this.DefaultFontSize;

m.Title = this.Title;
m.TitleColor = this.TitleColor.ToOxyColor();
m.TitleFont = this.TitleFont;
m.TitleFontSize = this.TitleFontSize;
m.TitleFontWeight = this.TitleFontWeight.ToOpenTypeWeight();
m.TitleToolTip = this.TitleToolTip;
//...

}
}

绘制的逻辑是这样的: XAML中增加了LineSeries, 触发OnSeriesChanged, 更新LogicTree, 再触发AppearanceChanged, 调用InvalidatePlot. 该方法是在PlotBase中定义的, 用了模板模式. Plot类重写了父类的UpdateModel. 多了Synchronize的步骤.

但WPF中的Series和LineSeries中并没有Render的代码, Render的逻辑还是在PlotModel中. SynchronizeProperties的作用就是将WPF中的属性同步给PlotModel, 然后由PlotModel绘制. 绘制在哪里, 绘制在PlotBase的Canvas上.

由于PlotBase继承自Control类, 而Control类在WPF是设计为无外观控件, 一般会从generic.xaml中获取默认的控件模板. WPF中有一个专用的OnApplyTemplate()方法, 如果需要在模板中查找元素并关联事件处理程序或添加数据绑定表达式, 应重写该方法. 详见WPF编程宝典18.3创建无外观控件一节. 这里PlotBase在OnApplyTemplate()中添加了绘制的纸和笔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// When overridden in a derived class, is invoked whenever application code or internal processes (such as a rebuilding layout pass)
/// call <see cref="M:System.Windows.Controls.Control.ApplyTemplate" /> . In simplest terms, this means the method is called
/// just before a UI element displays in an application. For more information, see Remarks.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.grid = this.GetTemplateChild(PartGrid) as Grid;
if (this.grid == null)
{
return;
}

this.canvas = new Canvas();
this.grid.Children.Add(this.canvas);
this.canvas.UpdateLayout();
this.renderContext = new CanvasRenderContext(this.canvas);

this.overlays = new Canvas();
this.grid.Children.Add(this.overlays);

this.zoomControl = new ContentControl();
this.overlays.Children.Add(this.zoomControl);

// add additional grid on top of everthing else to fix issue of mouse events getting lost
// it must be added last so it covers all other controls
var mouseGrid = new Grid();
mouseGrid.Background = Brushes.Transparent; // background must be set for hit test to work
this.grid.Children.Add(mouseGrid);
}